当父组件被重新渲染的时候,也会触发子组件的重新渲染,这样就多出了无意义的性能开销。如果子组件的状态没有发生变化,则子组件是不需要被重新渲染的。
我们可以使用 React.memo 来解决上述的问题,从而达到提高性能的目的。
注意:React.memo不是hooks,而是react的api。
React.memo 的语法格式如下:
xxxxxxxxxx11const 组件 = React.memo(函数式组件)例如,在下面的代码中,父组件声明了 count 和 flag 两个状态,子组件依赖于父组件通过 props 传递的 num。当父组件修改 flag 的值时,会导致子组件的重新渲染:
xxxxxxxxxx321import React, { useEffect, useState } from 'react'23// 父组件4export const Father: React.FC = () => {5 // 定义 count 和 flag 两个状态6 const [count, setCount] = useState(0)7 const [flag, setFlag] = useState(false)89 return (10 <>11 <h1>父组件</h1>12 <p>count 的值是:{count}</p>13 <p>flag 的值是:{String(flag)}</p>14 <button onClick={() => setCount((prev) => prev + 1)}>+1</button>15 <button onClick={() => setFlag((prev) => !prev)}>Toggle</button>16 <hr />17 <Son num={count} />18 </>19 )20}2122// 子组件:依赖于父组件通过 props 传递进来的 num23export const Son: React.FC<{ num: number }> = ({ num }) => {24 useEffect(() => {25 console.log('触发了子组件的渲染')26 })27 return (28 <>29 <h3>子组件 {num}</h3>30 </>31 )32}
我们使用 React.memo(函数式组件) 将子组件包裹起来,只有子组件依赖的 props 发生变化的时候,才会触发子组件的重新渲染。示例代码如下:
xxxxxxxxxx131// 子组件:依赖于父组件通过 props 传递进来的 num2export const Son: React.FC<{ num: number }> = React.memo(({ num }) => {3 4 useEffect(() => {5 console.log('触发了子组件的渲染')6 })7 8 return (9 <>10 <h3>子组件 --- {num}</h3>11 </>12 )13})也可以直接从react导入类型和方法,简写成这样:
xxxxxxxxxx151import type { FC } from "react"2import { memo, useEffect } from "react"34export const Son: FC<{ num: number }> = memo(({ num }) => {5 6 useEffect(() => {7 console.log('触发了子组件的渲染')8 })9 10 return (11 <>12 <h3>子组件 --- {num}</h3>13 </>14 )15})

进一步改造前面的案例:我们希望在 Father 组件中添加一个“计算属性”,根据 flag 值的真假,动态返回一段文本内容,并把计算的结果显示到页面上。示例代码如下:
xxxxxxxxxx251// 父组件2export const Father: React.FC = () => {3 // 定义 count 和 flag 两个状态4 const [count, setCount] = useState(0)5 const [flag, setFlag] = useState(false)67 // 根据布尔值进行计算,动态返回内容8 const tips = () => {9 console.log('触发了 tips 的重新计算')10 return flag ? <p>哪里贵了,不要睁着眼瞎说好不好</p> : <p>这些年有没有努力工作,工资涨没涨</p>11 }1213 return (14 <>15 <h1>父组件</h1>16 <p>count 的值是:{count}</p>17 <p>flag 的值是:{String(flag)}</p>18 {tips()}19 <button onClick={() => setCount((prev) => prev + 1)}>+1</button>20 <button onClick={() => setFlag((prev) => !prev)}>Toggle</button>21 <hr />22 <Son num={count} />23 </>24 )25}
代码编写完毕后,我们点击父组件中的 +1 按钮,发现 count 在自增,而 flag 的值不会发生变化。此时也会触发 tips 函数的重新执行,这就造成了性能的浪费。我们希望如果 flag 没有发生变化,则避免 tips 函数的重新计算,从而优化性能。此时需要用到 React Hooks 提供的 useMemo API。
useMemo 的语法格式如下:
xxxxxxxxxx51const memorizedValue = useMemo(cb, array)23const memoValue = useMemo(() => {4 return 计算得到的值5}, [value]) // 表示监听 value 的变化其中:
cb:这是一个函数,用于处理计算的逻辑,必须使用 return 返回计算的结果
array:这个数组中存储的是依赖项,只要依赖项发生变化,都会触发 cb 的重新执行。使用 array 需要注意以下几点
导入 useMemo:
xxxxxxxxxx11import React, { useEffect, useState, useMemo } from 'react'在 Father 组件中,使用 useMemo 对 tips 进行改造:
xxxxxxxxxx51// 根据布尔值进行计算,动态返回内容2const tips = useMemo(() => {3 console.log('触发了 tips 的重新计算')4 return flag ? '哪里贵了,不要睁着眼瞎说好不好' : '这些年有没有努力工作,工资涨没涨'5}, [flag])xxxxxxxxxx281import React, { useState, useEffect, useMemo } from "react";23export const Father: React.FC = () => {4 const [count, setCount] = useState(0);5 const [flag, setFlag] = useState(false);67 const tips = useMemo(() => {8 console.log("触发了 tips 的重新计算");9 return flag ? <p>哪里贵了,不要睁着眼瞎说好不好</p> : <p>这些年有没有努力工作,工资涨没涨</p>;10 }, [flag]);1112 return (13 <>14 <h1>父组件</h1>15 <p>count 的值为:{count}</p>16 <p>flag 的值为:{String(flag)}</p>17 18 <!--注意:tips不再是一个函数了,而是useMemo的返回值。-->19 {tips}20 21 <button onClick={() => setCount((prev) => prev + 1)}>点我+1</button>22 <button onClick={() => setFlag((prev) => !prev)}>Toggle</button>2324 <hr />25 <Son num={count} />26 </>27 );28};
此时,点击 Father 中的 +1 按钮,并不会触发 tips 的重新计算,而是会使用上一次缓存的值进行渲染。只有依赖项 flag 变化时,才会触发 tips 的重新计算。
之前我们所学的 useMemo 能够达到缓存某个变量值的效果,而当前要学习的 useCallback 用来对组件内的函数进行缓存,它返回的是缓存的函数。它的语法格式如下:
xxxxxxxxxx11const memoCallback = useCallback(cb, array)useCallback 会返回一个 memorized 回调函数供组件使用,从而防止组件每次 rerender 时反复创建相同的函数,能够节省内存开销,提高性能。其中:
cb 是一个函数,用于处理业务逻辑,这个 cb 就是需要被缓存的函数
array 是依赖项列表,当 array 中的依赖项变化时才会重新执行 useCallback。
接下来,我们通过下面的例子演示使用 useCallback 的必要性:当输入框触发 onChange 事件时,会给 kw 重新赋值。kw 值的改变会导致组件的 rerender,而组件的 rerender 会导致反复创建 onKwChange 函数并添加到 Set 集合中,造成了不必要的内存浪费。代码如下:
xxxxxxxxxx261import React, { useState, useCallback } from 'react'23// 用来存储函数的 set 集合4const set = new Set()56export const Search: React.FC = () => {7 const [kw, setKw] = useState('')89 const onKwChange = (e: React.ChangeEvent<HTMLInputElement>) => {10 setKw(e.currentTarget.value)11 }1213 // 把 onKwChange 函数的引用,存储到 set 集合中14 set.add(onKwChange)15 // 打印 set 集合中元素的数量16 console.log('set 中函数的数量为:' + set.size)1718 return (19 <>20 <input type="text" value={kw} onChange={onKwChange} />21 <hr />22 <p>{kw}</p>23 <p></p>24 </>25 )26}
运行上面的代码,我们发现每次文本框的值发生变化,都会打印 set.size 的值,而且这个值一直在自增 +1,因为每次组件 rerender 都会创建一个新的 onKwChange 函数添加到 set 集合中。
为了防止 Search 组件 rerender 时每次都会重新创建 onKwChange 函数,我们可以使用 useCallback 对这个函数进行缓存。改造后的代码如下:
xxxxxxxxxx271import React, { useState, useCallback } from 'react'23// 用来存储函数的 set 集合4const set = new Set()56export const Search: React.FC = () => {7 const [kw, setKw] = useState('')89 const onKwChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {10 setKw(e.currentTarget.value)11 // 第二个参数是空数组,那么只在组件初始化时执行一次。12 }, [])1314 // 把 onKwChange 函数的引用,存储到 set 集合中15 set.add(onKwChange)16 // 打印 set 集合中元素的数量17 console.log('set 中函数的数量为:' + set.size)1819 return (20 <>21 <input type="text" value={kw} onChange={onKwChange} />22 <hr />23 <p>{kw}</p>24 <p></p>25 </>26 )27}
运行改造后的代码,我们发现无论 input 的值如何发生变化,每次打印的 set.size 的值都是 1。证明我们使用 useCallback 实现了对函数的缓存。
另外一个对于input的onChange绑定事件的优化,就是直接在onChange属性里面写函数,这样应该不会生成多个函数,可以试一试。
导入需要的 hooks 函数,并定义需要的 TS 类型:
xxxxxxxxxx61import React, { useEffect, useState, useCallback } from 'react'23// 文本框组件的 props 类型4type SearchInputType = { onChange: (e: React.ChangeEvent) => void }5// 单词对象的 TS 类型6type WordType = { id: number; word: string }定义 SearchInput 搜索框子组件,接收父组件传递进来的 onChange 处理函数,每当 input 触发 onChange 事件时,调用 props.onChange 进行处理:
xxxxxxxxxx81// 子组件2const SearchInput: React.FC = (props) => {3 useEffect(() => {4 console.log('触发了 SearchInput 的 rerender')5 })67 return <input onChange={props.onChange} placeholder="请输入搜索关键字" />8}定义 SearchResult 搜索结果子组件,接收父组件传递进来的 query 搜索关键字,在 useEffect 中监听 props.query 的变化,从而请求搜索的结果:
xxxxxxxxxx291// 子组件:搜索结果2type wordType = {3 id: number;4 word: string;5}67type wordListType = Array<wordType>89const SearchResult: FC<{ query: string }> = (props) => {1011 const [list, setList] = useState<wordListType>([])1213 useEffect(() => {14 // 如果 query 为空字符串,则清空当前的列表15 if (!props.query) return setList([])16 17 // 查询数据18 fetch("https://api.liulongbin.top/v1/words?kw=" + props.query).then(res => res.json()).then(res => {19 // 为列表赋值20 setList(res.data)21 })2223 }, [props.query])2425 // 渲染列表数据26 return <>27 {list.map((item) => <p key={item.id}>{ item.word }</p>)}28 </>29}定义父组件 SearchBox 并渲染 SearchInput 组件和 SearchResult 组件。在父组件中监听 SearchInput 的 onChange 事件,并把父组件中定义的处理函数 onKwChange 传递进去。同时,把父组件中定义的搜索关键字 kw 传递给 SearchResult 组件。示例代码如下:
xxxxxxxxxx161// 父组件2export const SearchBox: React.FC = () => {3 const [kw, setKw] = useState('')45 const onKwChange = (e: React.ChangeEvent<HTMLInputElement>) => {6 setKw(e.currentTarget.value)7 }89 return (10 <div style={{ height: 500 }}>11 <SearchInput onChange={onKwChange} />12 <hr />13 <SearchResult query={kw} />14 </div>15 )16}
经过测试后,我们发现:
SearchResult子组件是需要跟随kw的变化重新渲染的,这个不用管。这里重点关注的是SearchInput子组件的更新过程。
props.onChange 把数据发送给父组件。kw 中。当 kw 发生变化,会触发父组件的 rerender。而父组件的 rerender 又会重新生成 onKwChange 函数并把函数的引用作为 props 传递给SearchInput子组件。
props 的变化,最终导致SearchInput子组件的 rerender。其实,SearchInput子组件根本不需要被重新渲染,因为 props.onChange 函数的处理逻辑没有发生变化,只是它的引用每次都在变。为了解决这个问题,我们需要用到 useCallback 和 React.memo。
首先,我们需要让子组件 SearchInput 被缓存,所以我们需要使用 React.memo 对其进行改造:
xxxxxxxxxx81// 子组件:搜索框2const SearchInput: React.FC<SearchInputType> = React.memo((props) => {3 useEffect(() => {4 console.log('触发了 SearchInput 的 rerender')5 })67 return <input onChange={props.onChange} placeholder="请输入搜索关键字" />8})使用 React.memo 对组件进行缓存后,如果子组件的 props 在两次更新前后没有任何变化,则被 memo 的组件不会 rerender。
所以为了实现 SearchInput 的缓存,还需要基于 useCallback 把父组件传递进来的 onChange 进行缓存。
在父组件中针对 onKwChange 调用 useCallback,示例代码如下:
xxxxxxxxxx31const onKwChange = useCallback((e: React.ChangeEvent) => {2 setKw(e.currentTarget.value)3}, [])经过测试,我们发现每当文本框内容发生变化,不会导致 SearchInput 组件的 rerender。

思路要理清楚,首先是onKwChange函数只需要生成一个,所以用到了useCallback方法,然后是SearchInput组件里面,父组件传递过来的属性onChange并没有发生变化,所以不需要重新渲染,于是用到了React.memo。
这套组合拳打的很经典,要灵活运用。
useTransition 可以将一个更新转为低优先级更新,使其可以被打断,不阻塞 UI 对用户操作的响应,能够提高用户的使用体验。它常用于优化视图切换时的用户体验。
例如有以下3个标签页组件,分别是 Home、Movie、About,其中 Movie 是一个渲染特别耗时的组件,在渲染 Movie 组件期间页面的 UI 会被阻塞,用户会感觉页面十分卡顿,示例代码如下:
xxxxxxxxxx641import React, { useState } from 'react'23export const TabsContainer: React.FC = () => {4 // 被激活的标签页的名字5 const [activeTab, setActiveTab] = useState('home')67 // 点击按钮,切换激活的标签页8 const onClickHandler = (tabName: string) => {9 setActiveTab(tabName)10 }1112 return (13 <div style={{ height: 500 }}>14 <TabButton isActive={activeTab === 'home'} onClick={() => onClickHandler('home')}>15 首页16 </TabButton>17 <TabButton isActive={activeTab === 'movie'} onClick={() => onClickHandler('movie')}>18 电影19 </TabButton>20 <TabButton isActive={activeTab === 'about'} onClick={() => onClickHandler('about')}>21 关于22 </TabButton>23 <hr />2425 {/* 根据被激活的标签名,渲染对应的 tab 组件 */}26 {activeTab === 'home' && <HomeTab />}27 {activeTab === 'movie' && <MovieTab />}28 {activeTab === 'about' && <AboutTab />}29 </div>30 )31}3233// Button 组件 props 的 TS 类型,因为要用到children属性,所以要添加上React.PropsWithChildren类型,然后是自定义的类型,很简单。34type TabButtonType = React.PropsWithChildren & { isActive: boolean; onClick: () => void }35// Button 组件36const TabButton: React.FC<TabButtonType> = (props) => {37 const onButtonClick = () => {38 props.onClick()39 }4041 return (42 <button className={['btn', props.isActive && 'active'].join(' ')} onClick={onButtonClick}>43 {props.children}44 </button>45 )46}4748// Home 组件49const HomeTab: React.FC = () => {50 return <>HomeTab</>51}5253// Movie 组件54const MovieTab: React.FC = () => {55 const items = Array(100000)56 .fill('MovieTab')57 .map((item, i) => <p key={i}>{item}</p>)58 return items59}6061// About 组件62const AboutTab: React.FC = () => {63 return <>AboutTab</>64}配套的 CSS 样式为:
xxxxxxxxxx151.btn {2 margin: 5px;3 background-color: rgb(8, 92, 238);4 color: #fff;5 transition: opacity 0.5s ease;6}78.btn:hover {9 opacity: 0.6;10 transition: opacity 0.5s ease;11}1213.btn.active {14 background-color: rgb(3, 150, 0);15}当movieTab里面是简单的字符串时,可以很轻松的切换:

当movieTab里面是大数据时,可以看到不光UI没有渲染出来,连button切换也不能进行了:

因为在默认情况下,react会先执行渲染操作而不是响应用户的操作,所以当切换到Movie组件的时候,react会先渲染Movie组件,但是Movie组件渲染非常耗时,所以这时候用户想切换到其它组件,react也是不会响应的。这时候就要用到useTransition这个hook。
xxxxxxxxxx61import { useTransition } from 'react';23function TabContainer() {4 const [isPending, startTransition] = useTransition();5 // ……6}参数:
useTransition() 时不需要传递任何参数返回值(数组):
isPending 布尔值:是否存在待处理的 transition,如果值为 true,说明页面上存在待渲染的部分,可以给用户展示一个加载的提示startTransition 函数:调用此函数,可以把状态的更新标记为低优先级的,不阻塞 UI 对用户操作的响应修改 TabsContainer 组件,使用 useTransition 把点击按钮后为 activeTab 赋值的操作,标记为低优先级。此时 React 会优先响应用户对界面的其它操作,从而保证 UI 不被阻塞:
xxxxxxxxxx161import React, { useState, useTransition } from 'react'23export const TabsContainer: React.FC = () => {4 // 被激活的标签页的名字5 const [activeTab, setActiveTab] = useState('home')6 const [, startTransition] = useTransition()78 // 点击按钮,切换激活的标签页9 const onClickHandler = (tabName: string) => {10 startTransition(() => {11 setActiveTab(tabName)12 })13 }1415 // 省略其它代码...16}此时,点击 Movie 按钮后,状态的更新被标记为低优先级,About 按钮的 hover 效果和点击操作都会被立即响应。

为了能够使用 isPending 的状态为按钮添加 loading 效果,我们需要把 useTransition 的调用从 TabsContainer 组件中挪到 TabButton 组件中:
xxxxxxxxxx211// Button 组件 props 的 TS 类型2type TabButtonType = React.PropsWithChildren & { isActive: boolean; onClick: () => void }34// Button 组件5const TabButton: React.FC<TabButtonType> = (props) => {6 const [isPending, startTransition] = useTransition()78 const onButtonClick = () => {9 startTransition(() => {10 props.onClick()11 })12 }1314 return (15 <button className={['btn', props.isActive && 'active'].join(' ')} onClick={onButtonClick}>16 {props.children}17 {/* 如果处于更新状态,则在对应按钮中渲染一个 loading 图标 */}18 {isPending && '...'}19 </button>20 )21}用老师提供的代码,可以看到isPending起作用了,按钮的内容发生了变化:
但是我的代码中,isPending没有起作用,界面没有按照预想发生变化,为什么呢?
xxxxxxxxxx511import React, { useState, useTransition } from "react";23const Home: React.FC = () => {4return <>HomeTab</>;5};67const Movie: React.FC<{ isPending: boolean }> = (props) => {89// 我在这里将传递过来的isPending输出,结果是false。说明刚开始渲染的时候,useTransition并没有将isPending设置为true,只有等到遇到后面的代码之后,才判断出要将isPending设置为true,而这个时候,Movie组件处在渲染过程中,除非是Movie组件重新渲染,否则下面这个判断永远不会执行。10console.log('movie组件 ',props.isPending);11if (props.isPending) return <p>数据获取中,请稍后...</p>;1213return Array(100000)14.fill("MovieTab")15.map((item, index) => <p key={index}>{item}</p>);16};1718const About: React.FC = () => {19return <>AboutTab</>;20};2122export const Demo: React.FC = () => {23const [active, setActive] = useState("home");24const [isPending, startTransition] = useTransition();2526const onClickTab = (tagName: string) => {27startTransition(() => {28setActive(tagName);29});30};3132return (33<>34{/* 按钮区 */}35<button className={"btn" + (active === "home" ? " active" : "")} onClick={() => onClickTab("home")}>36home37</button>38<button className={"btn" + (active === "movie" ? " active" : "")} onClick={() => onClickTab("movie")}>39movie40</button>41<button className={"btn" + (active === "about" ? " active" : "")} onClick={() => onClickTab("about")}>42about43</button>44<hr />45{/* 展示区 */}46{active === "home" ? <Home /> : ""}47{active === "movie" ? isPending ? "数据请求中,请稍后" : <Movie /> : ""}48{active === "about" ? <About /> : ""}49</>50);51};在Movie组件中,将父组件中传递过来的isPending输出,结果是false。说明刚开始渲染的时候,useTransition并没有将isPending设置为true,只有等到遇到后面的代码之后,才判断出要将isPending设置为true,而这个时候,Movie组件处在渲染过程中,那么isPending判断的那段代码是永远不会执行的。
可以看到,在点击movie按钮时,传递过来的isPending是false,切换到别的tab时,isPending的值是true,说明只有遇到费时的渲染代码时,react才会将isPending设置为true。
为什么老师代码里面可以使用isPending呢?因为useTransition定义在button组件中,在点击button时,绑定了startTransition事件,这样会触发父组件的
setActiveTab,会造成父组件的重新渲染,里面的子组件也会重新渲染,此时定义在button里面的isPending就被设置为true了,所以可以正常使用。那么我这样说看可不可以:isPending只能用在当前组件中,如果通过props传递,会有状态的延迟而达不到效果。
那如果就是想在Movie组件里面设置loading界面,应该怎么做呢?我想的话,应该不能将isPending状态定义在父组件中,这样传递给子组件的值就没有时间进行更改,应该定义在子组件中,然后传递给父组件,再通过父组件传递给另外的需要用到isPending的子组件。这个方法行不通。为什么呢?还是isPending值不是最新的问题,只有再次进入到button组件中,isPending才是最新值,此时该怎么执行组件的事件呢?我刚才是在onClick的时候传值的,现在呢?该怎么传值?
xxxxxxxxxx751// 这里采用了从button组件传递isPending状态出来,但很明显,这个值不是最新值。23import React, { useState, useTransition } from "react";45const Home: React.FC = () => {6return <>HomeTab</>;7};89const Movie: React.FC<{ isPending: boolean }> = (props) => {10console.log("props.isPending ", props.isPending);11if (props.isPending) return <p>数据请求中,请稍后</p>;12return Array(100000).fill("MovieTab").map((item, index) => <p key={index}>{item}</p>);13};1415const About: React.FC = () => {16return <>AboutTab</>;17};1819// Button 组件 props 的 TS 类型20type TabButtonType = React.PropsWithChildren & { isActive: boolean; onClick: (isPending: boolean) => void };2122// Button 组件23const TabButton: React.FC<TabButtonType> = (props) => {24const [isPending, startTransition] = useTransition();2526const onButtonClick = () => {27startTransition(() => {28props.onClick(isPending);29});30};3132return (33<button className={["btn", props.isActive && "active"].join(" ")} onClick={onButtonClick}>34{props.children}35{/* 如果处于更新状态,则在对应按钮中渲染一个 loading 图标 */}36{isPending && "..."}37</button>38);39};4041let count = 0;42export const TabsContainer: React.FC = () => {43// 被激活的标签页的名字44const [activeTab, setActiveTab] = useState("home");45const [pending, setPending] = useState(false);4647// 点击按钮,切换激活的标签页48const onClickHandler = (tabName: string, isPending: boolean) => {49setActiveTab(tabName);50setPending(isPending);51};5253console.log("父组件执行了几次? ", count);54count++;5556return (57<div style={{ height: 500 }}>58<TabButton isActive={activeTab === "home"} onClick={(isPending: boolean) => onClickHandler("home", isPending)}>59首页60</TabButton>61<TabButton isActive={activeTab === "movie"} onClick={(isPending: boolean) => onClickHandler("movie", isPending)}>62电影63</TabButton>64<TabButton isActive={activeTab === "about"} onClick={(isPending: boolean) => onClickHandler("about", isPending)}>65关于66</TabButton>67<hr />6869{/* 根据被激活的标签名,渲染对应的 tab 组件 */}70{activeTab === "home" && <Home />}71{activeTab === "movie" && <Movie isPending={pending} />}72{activeTab === "about" && <About />}73</div>74);75};上面这种写法更加复杂,从button中获取pending状态,再传递到Movie组件中,是最新值吗?
我看视频里面老师是这么做的,在TabsContainer组件里面定义专门的渲染组件的函数:
xxxxxxxxxx181// 用于渲染标签页的函数2const renderTabs = () =>{3if(isPending) return <h3>Loading</h3>4switch(activeTab){5case 'home':6return <Home />7case 'movie':8return <Movie />9case 'about':10return <About />11default:12return <Home />13}14}1516// 调用 renderTabs 函数,渲染标签页到组件中17{/* 标签页区域 */}18{renderTabs()}效果非常好:
仔细看老师的代码,和我的代码有什么不同?我在上面有一段代码
{active === "movie" ? isPending ? "数据请求中,请稍后" : <Movie /> : ""},老师是先判断isPending的,然后再渲染具体的组件,我是先判断应该渲染哪个组件,然后判断isPending,先将我的代码按照老师的顺序测试一下,看效果。为什么我的这种判断方式不行呢?尝试输出一下isPending和active的值,看一下到底是怎么变化的:
可以看到isPending为true时,active的状态是原来的状态,isPending为false了,active才会变为最新值。那么按照我的判断条件:
{active === "movie" ? isPending ? "数据请求中,请稍后" : <Movie /> : ""},active变为movie了,isPending就为false了,loading状态只有在isPending从false变为true再变为false时才会显示,从上面可以看出,这个时间非常短,根本达不到loading的需求。而renderTabs()方法里面,当isPending变为true之后,页面渲染的就是loading...,后来isPending变为了false,就会渲染耗时的组件,而直到这个组件真正渲染到页面上之前,原来的界面loading...是不会注销的,就相当于间接达到了目的。
这整个流程一定要搞清楚,我根本没有想到耗时组件渲染之前的情况是什么样的,考虑到这一点就好理解了。
那么先判断isPending,再渲染组件,写成这样行不行呢?
xxxxxxxxxx31{isPending ? '数据请求中,请稍后' : active === 'home' && <Home />}2{isPending ? '数据请求中,请稍后' : active === 'movie' && <Movie />}3{isPending ? '数据请求中,请稍后' : active === 'my' && <My />}这样是用到了pending,但是在home切换到movie的时候,会有三个“数据请求中,请稍后”,因为此时三个表达式都满足条件,所以都会被渲染出来。还是要按照老师的,写一个函数出来,首先就判断isPending,然后再分别渲染组件。
startTransition 的函数必须是同步的。React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition。如果在其执行期间,尝试稍后执行状态更新(例如在一个定时器中执行状态更新),这些状态更新不会被标记为 transition。xxxxxxxxxx91// 比如说,将切换tab的方法放在setTimeout中,这些状态更新是不会被标记为transition的,也就是会卡顿23const onBtnClick = (name: string) => {4 startTransition(() => {5 setTimeout(() => {6 setActiveTab(name)7 }, 100)8 })9}
在搜索框案例中,SearchResult 组件会根据用户输入的关键字,循环生成大量的 p 标签,因此它是一个渲染比较耗时的组件。代码如下:
xxxxxxxxxx281import React, { useState } from 'react'23// 父组件4export const SearchBox: React.FC = () => {5 const [kw, setKw] = useState('')67 const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {8 setKw(e.currentTarget.value)9 }1011 return (12 <div style={{ height: 500 }}>13 <input type="text" value={kw} onChange={onInputChange} />14 <hr />15 <SearchResult query={kw} />16 </div>17 )18}1920// 子组件,渲染列表项21const SearchResult: React.FC<{ query: string }> = (props) => {22 if (!props.query) return23 const items = Array(40000)24 .fill(props.query)25 .map((item, i) => <p key={i}>{item}</p>)2627 return items28}
可以看到,刷新浏览器之后我就输入了,很久之后才有反应。渲染还是非常耗时的,组件对输入的每一种情况都给出了响应。但很可能用户需要的并不是每一种情况都给出响应,而是输出一段话之后,根据这段话响应就行了。
注意,此案例不能使用 useTransition 进行性能优化,因为 useTransition 会把状态更新标记为低优先级,被标记为 transition 的状态更新将被其他状态更新打断。因此在高频率输入时,会导致中间的输入状态丢失的问题。比如说输入了123,在输入1会触发setKw(e.currentTarget.value)这个方法,但是这个状态更新是被startTransition包裹了,所以会异步执行,那么当用户很快输入2时,react会优先响应用户操作,这个状态更新会被UI更新打断,然后如果用户很快输入3,前面的更新也会被打断,最后输入框里面就可能只剩下3来显示了。
例如:
xxxxxxxxxx331import React, { useState, useTransition } from 'react'23// 父组件4export const SearchBox: React.FC = () => {5 const [kw, setKw] = useState('')6 // 1. 调用 useTransition 函数7 const [, startTransition] = useTransition()89 const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {10 // 2. 将文本框状态更新标记为“低优先级”,会导致中间的输入状态丢失11 startTransition(() => {12 setKw(e.currentTarget.value)13 })14 }1516 return (17 <div style={{ height: 500 }}>18 <input type="text" value={kw} onChange={onInputChange} />19 <hr />20 <SearchResult query={kw} />21 </div>22 )23}2425// 子组件,渲染列表项26const SearchResult: React.FC<{ query: string }> = (props) => {27 if (!props.query) return28 const items = Array(40000)29 .fill(props.query)30 .map((item, i) => <p key={i}>{item}</p>)3132 return items33}
我快速输入了123456,结果界面值渲染了6。
这里有一个现象,就是input组件直接写在SearchBox里面的时候,是会产生输入丢失的现象,但是如果将输入框作为一个单独的组件,使用useTransition,并没有输入丢失的现象:
xxxxxxxxxx281import type { FC } from 'react'2import { useState, useTransition } from 'react'34export const SearchBox: FC = () => {5const [kw, setKw] = useState("")6const [, startTransition] = useTransition();78const onKwChange = (e: React.ChangeEvent<HTMLInputElement>) => {9startTransition(() => {10setKw(e.currentTarget.value)11})12}13return <>14<SearchInput onChange={onKwChange} />15<hr />16<SearchResult query={kw} />17</>18}1920const SearchInput: FC<{ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void }> = (props) => {21return <input type="text" placeholder='请输入关键字搜索' onChange={props.onChange} />22}2324const SearchResult: FC<{ query: string }> = (props) => {25if (!props.query) return26const items = Array(30000).fill(props.query).map((item, index) => <p key={index}>{item}</p>)27return items;28}
这是为什么呢?暂时不知道原因。
useDeferredValue 提供一个 state 的延迟版本,根据其返回的延迟的 state 能够推迟更新 UI 中的某一部分,从而达到性能优化的目的。语法格式如下:
xxxxxxxxxx81import { useState, useDeferredValue } from 'react';23function SearchPage() {4 const [kw, setKw] = useState('');5 // 根据 kw 得到延迟的 kw6 const deferredKw = useDeferredValue(kw);7 // ...8}useDeferredValue 的返回值为一个延迟版的状态:
按需导入 useDeferredValue 这个 hooks API,并基于它进行搜索功能的性能优化:
xxxxxxxxxx331// 1. 按需导入 useDeferredValue 这个 Hooks API2import React, { useState, useDeferredValue } from 'react'34// 父组件5export const SearchBox: React.FC = () => {6 const [kw, setKw] = useState('')7 // 2. 基于 kw 的值,为其创建出一个延迟版的 kw 值,命名为 deferredKw8 const deferredKw = useDeferredValue(kw)910 const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {11 setKw(e.currentTarget.value)12 }1314 return (15 <div style={{ height: 500 }}>16 <input type="text" value={kw} onChange={onInputChange} />17 <hr />18 {/* 3. 将延迟版的 kw 值,传递给子组件使用 */}19 <SearchResult query={deferredKw} />20 </div>21 )22}2324// 子组件,渲染列表项25// 4. 子组件必须使用 React.memo() 进行包裹,这样当 props 没有变化时,会跳过子组件的 rerender26const SearchResult: React.FC<{ query: string }> = React.memo((props) => {27 if (!props.query) return28 const items = Array(40000)29 .fill(props.query)30 .map((item, i) => <p key={i}>{item}</p>)3132 return items33})
虽然渲染还是耗时,但是对用户的响应做到了只对最后的输入做出响应,这也是一种优化。
当 kw 的值频繁更新时,deferredKw 的值会明显滞后,此时用户在页面上看到的列表数据并不是最新的,为了防止用户感到困惑,我们可以给内容添加 opacity 透明度,表明当前看到的内容已过时。示例代码如下:
xxxxxxxxxx241// 1. 按需导入 useDeferredValue 这个 Hooks API2import React, { useState, useDeferredValue } from 'react'34// 父组件5export const SearchBox: React.FC = () => {6 const [kw, setKw] = useState('')7 // 2. 基于 kw 的值,为其创建出一个延迟版的 kw 值8 const deferredValue = useDeferredValue(kw)910 const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {11 setKw(e.currentTarget.value)12 }1314 return (15 <div style={{ height: 500 }}>16 <input type="text" value={kw} onChange={onInputChange} />17 <hr />18 {/* 3. 将延迟版的 kw 值,传递给子组件使用 */}19 <div style={{ opacity: kw !== deferredValue ? 0.3 : 1, transition: 'opacity 0.5s ease' }}>20 <SearchResult query={deferredValue} />21 </div>22 </div>23 )24}
效果还是蛮好的。

props的类型有时候这样写,有时候写成React.FC<React.PropsWithChildren>,要分清楚情况,后面这种情况是因为在子组件里面使用了{props.children}这样的代码,好好总结一下。
-----2024-02-19解答:
这是泛型的使用,不要和泛型的定义搞混了,在React.FC这个类型中,定义了一个泛型参数P,而这个P是使用者自己来定义的类型,在使用泛型的时候,需要传递一个类型参数,无论是上面图中的React.FC<{isPending:boolean}>,还是React.FC<React.PropsWithChildren>,还是下面更复杂的:
xxxxxxxxxx81type TabButtonType = React.PropsWithChildren & { isActive: boolean; onClick:(isPending: boolean) => void };23const TabButton:React.FC<TabButtonType> = (props) => {4 5 return <>6 <button>{props.children}</button>7 </>8}其实都是在自定义泛型P,都是用在props中的。
像上面定义input的onChange事件函数的时候,就是泛型的使用,而不是定义:
xxxxxxxxxx51// 这里为函数的参数e定义了类型,是React.ChangeEvent的类型,但是React.ChangeEvent这个类型使用了泛型,所以更具体参数类型的需要显式声明类型进去,所以就将HTMLInputElement这个类型传进去了。2// 这里是泛型的使用,而不是泛型的定义,这一点要搞清楚。3const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {4 setKw(e.currentTarget.value)5}还有定义组件类型的时候,也是泛型的使用:
xxxxxxxxxx111// 这里为组件定义了类型为FC,但FC里面其实是有很多属性的,所以传递属性需要用到泛型,那么FC这个类型里面就使用了泛型参数,因为props是对象的形式,所以这里写成对象的形式。2// 这里是泛型的使用。3const Son: FC<{ count: number }> = (props) => {4 useEffect(() => {5 console.log('子组件重新渲染了')6 })7 return <>8 <h2>Son子组件</h2>9 <p>传递过来的count值为:{props.count}</p>10 </>11}
那么定义泛型的时候,有什么不同的地方呢?现在我还说不上来,就现在心里把泛型的定义和使用分开来看待就行了。
&&我只知道是一个逻辑判断运算符,表示“逻辑与”。但是用在react中是什么意思呢?经常看到这种用法:{isPending && "..."},这是什么意思?
这不是react专门的用法,而是JS的用法。
在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false。我之前的用法只是两个boolean值来进行计算,比如if(isActive && data.length > 0){},所以没有想到会有这种效果。

在react中,&&右边的expression可以是字符串,也可以是一个组件。
在学习typescript的时候,定义的数组都是基本类型,比如说:(number | srting)[]或者Array<number | string>,那复杂一点的对象数组该怎么定义类型呢?其实就是为数组加上对象类型就行了,比如说:
xxxxxxxxxx101type wordType = {2 id:number;3 word:string;4}56type wordListType = wordType[];7// 或者8type wordListType = Array<wordType>;9// 也可以直接这样写10type wordListType = {id:number;word:string}[]
在使用useState定义数据的时候,基本类型的数据还好说,直接定义,ts会自动为我们推导出类型,但是复杂一点的数据,比如说对象数组、对象等等,不给出类型直接定义,在使用的时候就会有红色波浪线的提示:

定义的list是一个对象数组,里面有一个属性word,在渲染的时候会用到,由于没有定义list的类型,所以会报红色波浪线。
那么该怎么定义数据类型呢?

从react官方的TS定义来看,useState需要传递一个泛型参数进去即可。
那么可以写成这样:

这样就OK了。
在使用函数返回值时,由于惯性的思维,想直接为这个变量定义类型,就像这样自然:
xxxxxxxxxx21let x:number = 1;2const y:string = 'ok';但是函数返回值有一点很不同的地方,就是ts里面的函数基本上已经定义好了参数类型和返回值类型,而且如果函数里面使用了泛型的话,那么调用函数的时候,只需要指定泛型类型即可,函数的参数和返回值都会用到泛型的类型,这样用起来就非常简洁了。
我这里记录一下我的一种操作,在定义zustand的store的时候,useStore是create函数的返回值,于是我经常在useStore的后面定义类型,可想而知这是不行的、错误的,我根本就不知道useStore的类型是什么,而不需要知道,只需要在create时候,传递泛型参数进去即可,create函数已经定义好了参数和返回值的类型,ts会帮助我们定义出useStore的类型,这里就不需要定义类型了。
xxxxxxxxxx91import { create } from "zustand";23const useStore = create<BearType>()((set,get) => {4 return {5 6 }7})89export default useStore;如果有什么顾虑,可以直接看阮一峰的教程,函数一章。
在编写single-store的时候,是这样定义类型的,对于我这个typescript初学者来说,确实容易记混:
xxxxxxxxxx171import type { StateCreator } from "zustand";23const createBearSlice: StateCreator<BearType> = (set, get) => {4 return {5 bears: 0,6 incrementBears: () => set((prevState) => ({ bears: prevState.bears + 1 })),7 decrementBearsByStep: (step = 10) => set((prevState) => ({ bears: prevState.bears - step })),8 resetBears: () => set({ bears: 0 }),9 asyncIncrementBears: () => {10 setTimeout(() => {11 get().incrementBears();12 }, 1000);13 },14 };15};1617export default createBearSlice;为什么这里是这样做呢?原因是这个返回值本质上是一个函数,实际上我们定义的是函数的类型,查看一下StateCreator的typescript原始定义:
xxxxxxxxxx31export type StateCreator<T, Mis extends [StoreMutatorIdentifier, unknown][] = [], Mos extends [StoreMutatorIdentifier, unknown][] = [], U = T> = ((setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>, getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>, store: Mutate<StoreApi<T>, Mis>) => U) & {2 $$storeMutators?: Mos;3};可以看到类型定义主体是这个:
xxxxxxxxxx11(setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>, getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>, store: Mutate<StoreApi<T>, Mis>) => U就是一个函数的类型定义。
实际上是没有看类型的原始定义、没有学透typescript造成的混乱。